package com.thx.common.util;

import java.applet.AppletContext;
import java.applet.AppletStub;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.JarURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.security.CodeSource;
import java.security.ProtectionDomain;
import java.security.cert.Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.Attributes.Name;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;

import javax.swing.JApplet;

/**
 * JarClassLoader.
 */
public class JarClassLoader extends ClassLoader {

  /**
   * VM parameter key to turn on logging to file or console.
   */
  public static final String KEY_LOGGER = "JarClassLoader.logger";

  /**
   * VM parameter key to define log level. Valid levels are defined in {@link LogLevel}. Default
   * value is {@link LogLevel#DEBUG}.
   */
  public static final String KEY_LOGGER_LEVEL = "JarClassLoader.logger.level";

  /**
   * VM parameter key to define log area. Valid areas are defined in {@link LogArea}. Default value
   * is {@link LogArea#ALL}. Multiple areas could be specified with ',' delimiter (no spaces!).
   */
  public static final String KEY_LOGGER_AREA = "JarClassLoader.logger.area";

  public enum LogLevel {
    ERROR, WARN, INFO, DEBUG
  }

  public enum LogArea {
    /**
     * Enable all logging areas.
     */
    ALL,
    /**
     * Configuration related logging. Enabled always.
     */
    CONFIG,
    /**
     * Enable JAR related logging.
     */
    JAR,
    /**
     * Enable class loading related logging.
     */
    CLASS,
    /**
     * Enable resource loading related logging.
     */
    RESOURCE,
    /**
     * Enable native libraries loading related logging.
     */
    NATIVE
  }

  /**
   * Sub directory name for temporary files.
   * <p/>
   * JarClassLoader extracts all JARs and native libraries into temporary files and makes the best
   * attempt to clean these files on exit. The sub directory is created in the directory defined in
   * a system property "java.io.tmpdir".
   * <p/>
   * Verify the content of this directory periodically and empty it if required. Temporary files
   * could accumulate there if application was killed.
   */
  public static final String TMP_SUB_DIRECTORY = "JarClassLoader";

  private File dirTemp;
  private PrintStream logger;
  private List<JarFileInfo> lstJarFile;
  private Set<File> hsDeleteOnExit;
  private Map<String, Class<?>> hmClass;
  private LogLevel logLevel;
  private Set<LogArea> hsLogArea;
  private boolean logConsole;
  private JApplet applet;

  /**
   * Default constructor. Defines system class loader as a parent class loader.
   */
  public JarClassLoader() {
    this(ClassLoader.getSystemClassLoader());
  }

  /**
   * Constructor.
   *
   * @param parent class loader parent.
   */
  public JarClassLoader(ClassLoader parent) {
    super(parent);
    initLogger();

    hmClass = new HashMap<String, Class<?>>();
    lstJarFile = new ArrayList<JarFileInfo>();
    hsDeleteOnExit = new HashSet<File>();

    // Prepare common for all protocols
    String strUrlTopJar = null;
    ProtectionDomain pdTop = getClass().getProtectionDomain();
    CodeSource cs = pdTop.getCodeSource();
    URL urlTopJar = cs.getLocation();
    String protocol = urlTopJar.getProtocol();

    // Work with different cases:
    JarFileInfo jarFileInfo = null;
    if ("http".equals(protocol) || "https".equals(protocol)) {
      // Protocol 'http' or 'https' - application launched from WebStart / JNLP or as Java applet
      try {
        // Convert:
        // urlTopJar = "http://.../MyApp.jar" --> connection
        // sun.net.www.protocol.http.HttpURLConnection
        // to
        // urlTopJar = "jar:http://.../MyApp.jar!/" --> connection java.net.JarURLConnection
        urlTopJar = new URL("jar:" + urlTopJar + "!/");
        JarURLConnection jarCon = (JarURLConnection) urlTopJar.openConnection();
        JarFile jarFile = jarCon.getJarFile();
        jarFileInfo = new JarFileInfo(jarFile, jarFile.getName(), null, pdTop, null);
        logInfo(LogArea.JAR, "Loading from top JAR: '%s' PROTOCOL: '%s'", urlTopJar, protocol);
      } catch (Exception ex) {
        // ClassCastException, IOException
        logError(LogArea.JAR, "Failure to load HTTP JAR: %s %s", urlTopJar, ex.toString());
        return;
      }
    }
    if ("file".equals(protocol)) {
      // Protocol 'file' - application launched from exploded dir or JAR
      // Decoding required for 'space char' in URL:
      // URL.getFile() returns "/C:/my%20dir/MyApp.jar" for "/C:/my dir/MyApp.jar"
      try {
        strUrlTopJar = URLDecoder.decode(urlTopJar.getFile(), "UTF-8");
      } catch (UnsupportedEncodingException ex) {
        logError(LogArea.JAR, "Failure to decode URL: %s %s", urlTopJar, ex.toString());
        return;
      }
      File fileJar = new File(strUrlTopJar);

      // Application is loaded from directory:
      if (fileJar.isDirectory()) {
        logInfo(LogArea.JAR, "Loading from exploded directory: %s", strUrlTopJar);
        return; // JarClassLoader completed its job
      }

      // Application is loaded from a JAR:
      try {
        jarFileInfo = new JarFileInfo(new JarFile(fileJar), fileJar.getName(), null, pdTop, null);
        logInfo(LogArea.JAR, "Loading from top JAR: '%s' PROTOCOL: '%s'", strUrlTopJar, protocol);
      } catch (IOException ex) {
        logError(LogArea.JAR, "Not a JAR: %s %s", strUrlTopJar, ex.toString());
        return;
      }
    }

    // FINALLY LOAD TOP JAR:
    try {
      if (jarFileInfo == null) {
        throw new IOException(String.format("Unknown protocol %s", protocol));
      }
      loadJar(jarFileInfo); // start recursive JAR loading
    } catch (IOException ex) {
      logError(LogArea.JAR, "Not valid URL: %s %s", urlTopJar, ex.toString());
      return;
    }

    checkShading();
    Runtime.getRuntime().addShutdownHook(new Thread() {
      public void run() {
        shutdown();
      }
    });
  } // JarClassLoader()

  // --------------------------------separator--------------------------------
  static int ______INIT;

  private void initLogger() {
    // Logger defaults:
    logConsole = true;
    this.logger = System.out; // default to console
    logLevel = LogLevel.ERROR;
    hsLogArea = new HashSet<LogArea>();
    hsLogArea.add(LogArea.CONFIG);

    // Logger stream console or file:
    String logger = System.getProperty(KEY_LOGGER);
    if (logger != null) {
      try {
        this.logger = new PrintStream(logger);
        logConsole = false;
      } catch (FileNotFoundException ex) {
        logError(LogArea.CONFIG, "Cannot create log file %s.", logger);
      }
    }

    // Logger level:
    String logLevel = System.getProperty(KEY_LOGGER_LEVEL);
    if (logLevel != null) {
      try {
        this.logLevel = LogLevel.valueOf(logLevel);
      } catch (Exception ex) {
        logError(LogArea.CONFIG, "Not valid parameter in %s=%s", KEY_LOGGER_LEVEL, logLevel);
      }
    }

    // Logger area:
    String logArea = System.getProperty(KEY_LOGGER_AREA);
    if (logArea != null) {
      String[] tokenAll = logArea.split(",");
      try {
        for (String t : tokenAll) {
          hsLogArea.add(LogArea.valueOf(t));
        }
      } catch (Exception ex) {
        logError(LogArea.CONFIG, "Not valid parameter in %s=%s", KEY_LOGGER_AREA, logArea);
      }
    }
    if (hsLogArea.size() == 1 && hsLogArea.contains(LogArea.CONFIG)) {
      for (LogArea la : LogArea.values()) {
        hsLogArea.add(la);
      }
    }
  } // initLogger()

  /**
   * Using temp files (one per inner JAR/DLL) solves many issues: 1. There are no ways to load JAR
   * defined in a JarEntry directly into the JarFile object (see also #6 below). 2. Cannot use
   * memory-mapped files because they are using nio channels, which are not supported by JarFile
   * ctor. 3. JarFile object keeps opened JAR files handlers for fast access. 4. Deep resource in a
   * jar-in-jar does not have well defined URL. Making temp file with JAR solves this problem. 5.
   * Similar issues with native libraries: <code>ClassLoader.findLibrary()</code> accepts ONLY
   * string with absolute path to the file with native library. 6. Option
   * "java.protocol.handler.pkgs" does not allow access to nested JARs(?).
   *
   * @param inf JAR entry information.
   * @return temporary file object presenting JAR entry.
   */
  private File createTempFile(JarEntryInfo inf) throws JarClassLoaderException {
    // Temp files directory:
    // WinXP: C:/Documents and Settings/username/Local Settings/Temp/JarClassLoader
    // Unix: /var/tmp/JarClassLoader
    if (dirTemp == null) {
      File dir = new File(System.getProperty("java.io.tmpdir"), TMP_SUB_DIRECTORY);
      if (!dir.exists()) {
        dir.mkdir();
      }
      chmod777(dir); // Unix - allow temp directory RW access to all users.
      if (!dir.exists() || !dir.isDirectory()) {
        throw new JarClassLoaderException("Cannot create temp directory " + dir.getAbsolutePath());
      }
      dirTemp = dir;
    }
    File fileTmp = null;
    try {
      fileTmp = File.createTempFile(inf.getName() + ".", null, dirTemp);
      fileTmp.deleteOnExit();
      chmod777(fileTmp); // Unix - allow temp file deletion by any user
      byte[] theBy = inf.getJarBytes();
      BufferedOutputStream os = new BufferedOutputStream(new FileOutputStream(fileTmp));
      os.write(theBy);
      os.close();
      return fileTmp;
    } catch (IOException ex) {
      throw new JarClassLoaderException(
          String.format("Cannot create temp file '%s' for %s", fileTmp, inf.jarEntry), ex);
    }
  } // createTempFile()

  /**
   * Loads specified JAR.
   */
  private void loadJar(JarFileInfo jarFileInfo) throws IOException {
    lstJarFile.add(jarFileInfo);
    try {
      Enumeration<JarEntry> en = jarFileInfo.jarFile.entries();
      final String jarExt = ".jar";
      while (en.hasMoreElements()) {
        JarEntry je = en.nextElement();
        if (je.isDirectory()) {
          continue;
        }
        String name = je.getName().toLowerCase(); // JarEntry name
        if (name.lastIndexOf(jarExt) == name.length() - jarExt.length()) {
          JarEntryInfo inf = new JarEntryInfo(jarFileInfo, je);
          File fileTemp = createTempFile(inf);
          logInfo(LogArea.JAR, "Loading inner JAR %s from temp file %s", inf.jarEntry,
              getFilename4Log(fileTemp));
          // Construct ProtectionDomain for this inner JAR:
          URL url = fileTemp.toURI().toURL();
          ProtectionDomain pdParent = jarFileInfo.pd;
          // 'csParent' is never null: top JAR has it, JCL creates it for child JAR:
          CodeSource csParent = pdParent.getCodeSource();
          Certificate[] certParent = csParent.getCertificates();
          CodeSource csChild = (certParent == null ? new CodeSource(url, csParent.getCodeSigners())
              : new CodeSource(url, certParent));
          ProtectionDomain pdChild = new ProtectionDomain(csChild, pdParent.getPermissions(),
              pdParent.getClassLoader(), pdParent.getPrincipals());
          loadJar(new JarFileInfo(new JarFile(fileTemp), inf.getName(), jarFileInfo, pdChild,
              fileTemp));
        }
      }
    } catch (JarClassLoaderException ex) {
      throw new RuntimeException("ERROR on loading inner JAR: " + ex.getMessageAll());
    }
  } // loadJar()

  private JarEntryInfo findJarEntry(String name) {
    for (JarFileInfo jarFileInfo : lstJarFile) {
      JarFile jarFile = jarFileInfo.jarFile;
      JarEntry jarEntry = jarFile.getJarEntry(name);
      if (jarEntry != null) {
        return new JarEntryInfo(jarFileInfo, jarEntry);
      }
    }
    return null;
  } // findJarEntry()

  private List<JarEntryInfo> findJarEntries(String name) {
    List<JarEntryInfo> lst = new ArrayList<JarEntryInfo>();
    for (JarFileInfo jarFileInfo : lstJarFile) {
      JarFile jarFile = jarFileInfo.jarFile;
      JarEntry jarEntry = jarFile.getJarEntry(name);
      if (jarEntry != null) {
        lst.add(new JarEntryInfo(jarFileInfo, jarEntry));
      }
    }
    return lst;
  } // findJarEntries()

  /**
   * Finds native library entry.
   *
   * @param lib Library name. For example for the library name "Native" the Windows returns entry
   *        "Native.dll", the Linux returns entry "libNative.so", the Mac returns entry
   *        "libNative.jnilib".
   * @return Native library entry.
   */
  private JarEntryInfo findJarNativeEntry(String lib) {
    String name = System.mapLibraryName(lib);
    for (JarFileInfo jarFileInfo : lstJarFile) {
      JarFile jarFile = jarFileInfo.jarFile;
      Enumeration<JarEntry> en = jarFile.entries();
      while (en.hasMoreElements()) {
        JarEntry je = en.nextElement();
        if (je.isDirectory()) {
          continue;
        }
        // Example: sName is "Native.dll"
        String entry = je.getName(); // "Native.dll" or "abc/xyz/Native.dll"
        // sName "Native.dll" could be found, for example
        // - in the path: abc/Native.dll/xyz/my.dll <-- do not load this one!
        // - in the partial name: abc/aNative.dll <-- do not load this one!
        String[] token = entry.split("/"); // the last token is library name
        if (token.length > 0 && token[token.length - 1].equals(name)) {
          logInfo(LogArea.NATIVE, "Loading native library '%s' found as '%s' in JAR %s", lib, entry,
              jarFileInfo.simpleName);
          return new JarEntryInfo(jarFileInfo, je);
        }
      }
    }
    return null;
  } // findJarNativeEntry()

  /**
   * Loads class from a JAR and searches for all jar-in-jar.
   *
   * @param className class to load.
   * @return Loaded class.
   */
  private Class<?> findJarClass(String className) throws JarClassLoaderException {
    Class<?> cls = hmClass.get(className);
    if (cls != null) {
      return cls;
    }
    // Char '/' works for Win32 and Unix.
    String name = className.replace('.', '/') + ".class";
    JarEntryInfo inf = findJarEntry(name);
    String jarSimpleName = null;
    if (inf != null) {
      jarSimpleName = inf.jarFileInfo.simpleName;
      definePackage(className, inf);
      byte[] theBy = inf.getJarBytes();
      try {
        cls = defineClass(className, theBy, 0, theBy.length, inf.jarFileInfo.pd);
      } catch (ClassFormatError ex) {
        throw new JarClassLoaderException(null, ex);
      }
    }

    if (cls == null) {
      throw new JarClassLoaderException(className);
    }
    hmClass.put(className, cls);
    logInfo(LogArea.CLASS, "Loaded %s by %s from JAR %s", className, getClass().getName(),
        jarSimpleName);
    return cls;
  } // findJarClass()

  private void checkShading() {
    if (logLevel.ordinal() < LogLevel.WARN.ordinal()) {
      // Do not waste time if no logging.
      return;
    }
    Map<String, JarFileInfo> hm = new HashMap<String, JarFileInfo>();
    for (JarFileInfo jarFileInfo : lstJarFile) {
      JarFile jarFile = jarFileInfo.jarFile;
      Enumeration<JarEntry> en = jarFile.entries();
      while (en.hasMoreElements()) {
        JarEntry je = en.nextElement();
        if (je.isDirectory()) {
          continue;
        }
        String entry = je.getName(); // "Some.txt" or "abc/xyz/Some.txt"
        if ("META-INF/MANIFEST.MF".equals(entry)) {
          continue;
        }
        JarFileInfo jar = hm.get(entry);
        if (jar == null) {
          hm.put(entry, jarFileInfo);
        } else {
          logWarn(LogArea.JAR, "ENTRY %s IN %s SHADES %s", entry, jar.simpleName,
              jarFileInfo.simpleName);
        }
      }
    }
  } // checkShading()

  // --------------------------------separator--------------------------------
  static int ______SHUTDOWN;

  /**
   * Called on shutdown to cleanup temporary files. JVM does not close handles to native libraries
   * files or JARs with resources loaded as getResourceAsStream(). Temp files are not deleted even
   * if they are marked deleteOnExit(). They also fail to delete explicitly. Workaround is to
   * preserve list with temp files in configuration file "[user.home]/.JarClassLoader" and delete
   * them on next application run. See http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4171239
   * "This occurs only on Win32, which does not allow a file to be deleted until all streams on it
   * have been closed."
   */
  private void shutdown() {
    for (JarFileInfo jarFileInfo : lstJarFile) {
      try {
        jarFileInfo.jarFile.close();
      } catch (IOException ex) {
        // Ignore. In the worst case temp files will accumulate.
      }
      File file = jarFileInfo.fileDeleteOnExit;
      if (file != null && !file.delete()) {
        hsDeleteOnExit.add(file);
      }
    }
    // Private configuration file with failed to delete temporary files:
    // WinXP: C:/Documents and Settings/username/.JarClassLoader
    // Unix: /export/home/username/.JarClassLoader
    // -or- /home/username/.JarClassLoader
    File fileCfg = new File(System.getProperty("user.home") + File.separator + ".JarClassLoader");
    deleteOldTemp(fileCfg);
    persistNewTemp(fileCfg);
  } // shutdown()

  /**
   * Deletes temporary files listed in the file. The method is called on shutdown().
   *
   * @param fileCfg file with temporary files list.
   */
  private void deleteOldTemp(File fileCfg) {
    BufferedReader reader = null;
    try {
      int count = 0;
      reader = new BufferedReader(new FileReader(fileCfg));
      String line;
      while ((line = reader.readLine()) != null) {
        File file = new File(line);
        if (!file.exists()) {
          continue; // already deleted; from command line?
        }
        if (file.delete()) {
          count++;
        } else {
          // Cannot delete, will try next time.
          hsDeleteOnExit.add(file);
        }
      }
      logDebug(LogArea.CONFIG, "Deleted %d old temp files listed in %s", count,
          fileCfg.getAbsolutePath());
    } catch (IOException ex) {
      // Ignore. This file may not exist.
    } finally {
      if (reader != null) {
        try {
          reader.close();
        } catch (IOException ex) {
          // 不需要处理
        }
      }
    }
  } // deleteOldTemp()

  /**
   * Creates file with temporary files list. This list will be used to delete temporary files on the
   * next application launch. The method is called from shutdown().
   *
   * @param fileCfg file with temporary files list.
   */
  private void persistNewTemp(File fileCfg) {
    if (hsDeleteOnExit.size() == 0) {
      logDebug(LogArea.CONFIG, "No temp file names to persist on exit.");
      fileCfg.delete(); // do not pollute disk
      return;
    }
    logDebug(LogArea.CONFIG, "Persisting %d temp file names into %s", hsDeleteOnExit.size(),
        fileCfg.getAbsolutePath());
    BufferedWriter writer = null;
    try {
      writer = new BufferedWriter(new FileWriter(fileCfg));
      for (File file : hsDeleteOnExit) {
        if (!file.delete()) {
          String path = file.getCanonicalPath();
          writer.write(path);
          writer.newLine();
          logWarn(LogArea.JAR, "JVM failed to release %s", path);
        }
      }
    } catch (IOException ex) {
      // Ignore. In the worst case temp files will accumulate.
    } finally {
      if (writer != null) {
        try {
          writer.close();
        } catch (IOException ex) {
          // 不需要
        }
      }
    }
  } // persistNewTemp()

  // --------------------------------separator--------------------------------
  static int ______ACCESS;

  /**
   * Checks how the application was loaded: from JAR or file system.
   *
   * @return true if application was started from JAR.
   */
  public boolean isLaunchedFromJar() {
    return (lstJarFile.size() > 0);
  } // isLaunchedFromJar()

  /**
   * Returns the name of the jar file main class, or null if no "Main-Class" manifest attributes was
   * defined.
   *
   * @return Main class declared in JAR's manifest.
   */
  public String getManifestMainClass() {
    Attributes attr = null;
    if (isLaunchedFromJar()) {
      try {
        // The first element in array is the top level JAR
        Manifest manifest = lstJarFile.get(0).jarFile.getManifest();
        attr = manifest.getMainAttributes();
      } catch (IOException ex) {
        // 不需要处理
      }
    }
    return (attr == null ? null : attr.getValue(Attributes.Name.MAIN_CLASS));
  } // getManifestMainClass()

  /**
   * Invokes main() method on class with provided parameters.
   *
   * @param clsName class name in form "MyClass" for default package or "com.abc.MyClass" for class
   *        in some package
   * @param args arguments for the main() method or null.
   * @throws Throwable wrapper for many exceptions thrown while
   *         <ul>
   *         <li>(1) main() method lookup: ClassNotFoundException, SecurityException,
   *         NoSuchMethodException</li>
   *         <li>(2) main() method launch: IllegalArgumentException, IllegalAccessException
   *         (disabled)</li>
   *         <li>(3) Actual cause of InvocationTargetException</li>
   *         </ul>
   *
   *         See
   *         {@link http://java.sun.com/developer/Books/javaprogramming/JAR/api/jarclassloader.html}
   *         and
   *         {@link http://java.sun.com/developer/Books/javaprogramming/JAR/api/example-1dot2/JarClassLoader.java}
   */
  public void invokeMain(String clsName, String[] args) throws Throwable {
    Class<?> clazz = loadClass(clsName);
    logInfo(LogArea.CONFIG, "Launch: %s.main(); Loader: %s", clsName, clazz.getClassLoader());
    Method method = clazz.getMethod("main", new Class<?>[] {String[].class});

    boolean validModifiers = false;
    boolean validVoid = false;

    if (method != null) {
      method.setAccessible(true); // Disable IllegalAccessException
      int modifiers = method.getModifiers(); // main() must be "public static"
      validModifiers = Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers);
      Class<?> clazzRet = method.getReturnType(); // main() must be "void"
      validVoid = (clazzRet == void.class);
    }
    if (method == null || !validModifiers || !validVoid) {
      throw new NoSuchMethodException("The main() method in class \"" + clsName + "\" not found.");
    }

    // Invoke method.
    // Crazy cast "(Object)args" because param is: "Object... args"
    try {
      method.invoke(null, (Object) args);
    } catch (InvocationTargetException ex) {
      throw ex.getTargetException();
    }
  } // invokeMain()

  /**
   * Call this method to initialize an applet from your launcher class
   * <code>MyAppletLauncher.init()</code> method.
   *
   * @param className class name in form "MyClass" for default package or "com.abc.MyClass" for
   *        class in some package
   * @param appletParent parent applet from a launcher.
   * @throws Throwable wrapper for many exceptions thrown while applet instantiation and calling
   *         init() method.
   */
  public void initApplet(String className, final JApplet appletParent) throws Throwable {
    Class<?> clazz = loadClass(className);
    logInfo(LogArea.CONFIG, "initApplet() --> %s.init(); Loader: %s", className,
        clazz.getClassLoader());
    applet = (JApplet) clazz.newInstance();
    applet.setStub(new AppletStub() {
      @Override
      public boolean isActive() {
        return appletParent.isActive();
      }

      @Override
      public URL getDocumentBase() {
        return appletParent.getDocumentBase();
      }

      @Override
      public URL getCodeBase() {
        return appletParent.getCodeBase();
      }

      @Override
      public String getParameter(String name) {
        return appletParent.getParameter(name);
      }

      @Override
      public AppletContext getAppletContext() {
        return appletParent.getAppletContext();
      }

      @Override
      public void appletResize(int width, int height) {
        appletParent.resize(width, height);
      }
    });
    applet.init();
    appletParent.setContentPane(applet.getContentPane());
  } // initApplet()

  /**
   * Call this method to start the applet from your launcher class
   * <code>MyAppletLauncher.start()</code> method.
   */
  public void startApplet() {
    checkApplet();
    logInfo(LogArea.CONFIG, "startApplet() --> %s.start()", applet.getClass().getName());
    applet.start();
  }

  /**
   * Call this method to stop the applet from your launcher class
   * <code>MyAppletLauncher.stop()</code> method.
   */
  public void stopApplet() {
    checkApplet();
    logInfo(LogArea.CONFIG, "stopApplet() --> %s.stop()", applet.getClass().getName());
    applet.stop();
  }

  /**
   * Call this method to destroy the applet from your launcher class
   * <code>MyAppletLauncher.destroy()</code> method.
   */
  public void destroyApplet() {
    checkApplet();
    logInfo(LogArea.CONFIG, "destroyApplet() --> %s.destroy()", applet.getClass().getName());
    applet.destroy();
  }

  // --------------------------------separator--------------------------------
  static int ______OVERRIDE;

  /**
   * Class loader JavaDoc encourages overriding findClass(String) in derived class rather than
   * overriding this method. This does not work for loading classes from a JAR. Default
   * implementation of loadClass() is able to load a class from a JAR without calling findClass().
   */
  @Override
  protected synchronized Class<?> loadClass(String className, boolean resolve)
      throws ClassNotFoundException {
    logDebug(LogArea.CLASS, "LOADING %s (resolve=%b)", className, resolve);
    // Each thread must have THIS class loader set as a context class loader.
    // This is required to prevent failure finding a class or resource from
    // external JAR requested by a common class loaded from rt.jar.
    // The best example is external LnF, explained in steps:
    // 1. Application requests 'javax.swing.JOptionPane'.
    // 2. THIS class loader passes request to system default class loader
    // to load the class from rt.jar.
    // 3. The class 'javax.swing.JOptionPane' is loaded by system default class
    // loader.
    // 4. The class 'javax.swing.JOptionPane' is requesting 'UIDefaults.getUI()'
    // for component, which resides in external LnF JAR.
    // 5. The class loader which is used to load the requested component is
    // current thread context class loader if it is set, otherwise the parent
    // thread context class loader, or the default system class loader
    // for the top level thread.
    // 6. The system class loader is used to load requested component if
    // thread context class loader is not set. The default system class loader is
    // - sun.misc.Launcher$AppClassLoader - run from file system or JAR
    // - com.sun.jnlp.JNLPClassLoader - run from JNLP
    // System class loaders cannot find requested component in external
    // JAR and throw exception.
    //
    // Setting thread context class loader for the top thread in invokeMain()
    // method is sufficient for most cases. It fails for new threads created
    // not from the main thread.
    //
    // Setting thread context class loader below must be reconsidered
    // for specific conditions.
    //
    // Essential reading:
    // - Thread.getContextClassLoader() JavaDoc.
    // - http://www.javaworld.com/javaworld/javaqa/2003-06/01-qa-0606-load.html
    Thread.currentThread().setContextClassLoader(this);

    Class<?> clazz = null;
    try {
      // Step 0. This class is already loaded by system classloader.
      if (getClass().getName().equals(className)) {
        return JarClassLoader.class;
      }
      // Step 1. Load from JAR.
      if (isLaunchedFromJar()) {
        try {
          clazz = findJarClass(className); // Do not simplify! See "finally"!
          return clazz;
        } catch (JarClassLoaderException ex) {
          if (ex.getCause() == null) {
            logDebug(LogArea.CLASS, "Not found %s in JAR by %s: %s", className,
                getClass().getName(), ex.getMessage());
          } else {
            logDebug(LogArea.CLASS, "Error loading %s in JAR by %s: %s", className,
                getClass().getName(), ex.getCause());
          }
          // keep looking...
        }
      }
      // Step 2. Load by parent (usually system) class loader.
      // Call findSystemClass() AFTER attempt to find in a JAR.
      // If it called BEFORE it will load class-in-jar using
      // SystemClassLoader and "infect" it with SystemClassLoader.
      // The SystemClassLoader will be used to load all dependent
      // classes. SystemClassLoader will fail to load a class from
      // jar-in-jar and to load dll-in-jar.
      try {
        // No need to call findLoadedClass(sClassName) because it's called inside:
        ClassLoader cl = getParent();
        clazz = cl.loadClass(className);
        // System classloader does not define ProtectionDomain->CodeSource - null
        logInfo(LogArea.CLASS, "Loaded %s by %s", className, cl.getClass().getName());
        return clazz;
      } catch (ClassNotFoundException ex) {
        // 不需要处理
      }
      // What else?
      throw new ClassNotFoundException("Failure to load: " + className);
    } finally {
      if (clazz != null && resolve) {
        resolveClass(clazz);
      }
    }
  } // loadClass()

  /**
   * @return A URL object for reading the resource, or null if the resource could not be found.
   *         Example URL: jar:file:C:\...\some.jar!/resources/InnerText.txt
   * @see java.lang.ClassLoader#findResource(java.lang.String)
   */
  @Override
  protected URL findResource(String name) {
    logDebug(LogArea.RESOURCE, "findResource: %s", name);
    if (isLaunchedFromJar()) {
      JarEntryInfo inf = findJarEntry(normalizeResourceName(name));
      if (inf != null) {
        URL url = inf.getUrl();
        logInfo(LogArea.RESOURCE, "found resource: %s", url);
        return url;
      }
      logInfo(LogArea.RESOURCE, "not found resource: %s", name);
      return null;
    }
    return super.findResource(name);
  } // findResource()

  /**
   * @return An enumeration of {@link java.net.URL <tt>URL</tt>} objects for the resources
   * @see java.lang.ClassLoader#findResources(java.lang.String)
   */
  @Override
  public Enumeration<URL> findResources(String name) throws IOException {
    logDebug(LogArea.RESOURCE, "getResources: %s", name);
    if (isLaunchedFromJar()) {
      List<JarEntryInfo> lstJarEntry = findJarEntries(normalizeResourceName(name));
      List<URL> lstUrl = new ArrayList<URL>();
      for (JarEntryInfo inf : lstJarEntry) {
        URL url = inf.getUrl();
        if (url != null) {
          lstUrl.add(url);
        }
      }
      return Collections.enumeration(lstUrl);
    }
    return super.findResources(name);
  } // findResources()

  /**
   * @return The absolute path of the native library.
   * @see java.lang.ClassLoader#findLibrary(java.lang.String)
   */
  @Override
  protected String findLibrary(String lib) {
    logDebug(LogArea.NATIVE, "findLibrary: %s", lib);
    if (isLaunchedFromJar()) {
      JarEntryInfo inf = findJarNativeEntry(lib);
      if (inf != null) {
        try {
          File file = createTempFile(inf);
          logDebug(LogArea.NATIVE, "Loading native library %s from temp file %s", inf.jarEntry,
              getFilename4Log(file));
          hsDeleteOnExit.add(file);
          return file.getAbsolutePath();
        } catch (JarClassLoaderException ex) {
          logError(LogArea.NATIVE, "Failure to load native library %s: %s", lib, ex.toString());
        }
      }
      return null;
    }
    return super.findLibrary(lib);
  } // findLibrary()

  // --------------------------------separator--------------------------------
  static int ______HELPERS;

  /**
   * The default <code>ClassLoader.defineClass()</code> does not create package for the loaded class
   * and leaves it null. Each package referenced by this class loader must be created only once
   * before the <code>ClassLoader.defineClass()</code> call. The base class <code>ClassLoader</code>
   * keeps cache with created packages for reuse.
   *
   * @param className class to load.
   * @throws IllegalArgumentException If package name duplicates an existing package either in this
   *         class loader or one of its ancestors.
   */
  private void definePackage(String className, JarEntryInfo inf) throws IllegalArgumentException {
    int pos = className.lastIndexOf('.');
    String packageName = pos > 0 ? className.substring(0, pos) : "";
    if (getPackage(packageName) == null) {
      JarFileInfo jfi = inf.jarFileInfo;
      definePackage(packageName, jfi.getSpecificationTitle(), jfi.getSpecificationVersion(),
          jfi.getSpecificationVendor(), jfi.getImplementationTitle(),
          jfi.getImplementationVersion(), jfi.getImplementationVendor(), jfi.getSealUrl());
    }
  }

  /**
   * The system class loader could load resources defined as "com/abc/Foo.txt" or "com\abc\Foo.txt".
   * This method converts path with '\' to default '/' JAR delimiter.
   *
   * @param name resource name including path.
   * @return normalized resource name.
   */
  private String normalizeResourceName(String name) {
    return name.replace('\\', '/');
  }

  private void chmod777(File file) {
    file.setReadable(true, false);
    file.setWritable(true, false);
    file.setExecutable(true, false); // Unix: allow content for dir, redundant for file
  }

  private String getFilename4Log(File file) {
    if (logger != null) {
      try {
        // In form "C:\Documents and Settings\..."
        return file.getCanonicalPath();
      } catch (IOException ex) {
        // In form "C:\DOCUME~1\..."
        return file.getAbsolutePath();
      }
    }
    return null;
  }

  private void checkApplet() {
    if (applet == null) {
      throw new IllegalStateException(
          "Applet is not inited. " + "Please call JarClassLoader.initApplet() first.");
    }
  }

  private void logDebug(LogArea area, String msg, Object... obj) {
    log(LogLevel.DEBUG, area, msg, obj);
  }

  private void logInfo(LogArea area, String msg, Object... obj) {
    log(LogLevel.INFO, area, msg, obj);
  }

  private void logWarn(LogArea area, String msg, Object... obj) {
    log(LogLevel.WARN, area, msg, obj);
  }

  private void logError(LogArea area, String msg, Object... obj) {
    log(LogLevel.ERROR, area, msg, obj);
  }

  private void log(LogLevel level, LogArea area, String msg, Object... obj) {
    if (level.ordinal() <= logLevel.ordinal()) {
      if (hsLogArea.contains(LogArea.ALL) || hsLogArea.contains(area)) {
        logger.printf("JarClassLoader-" + level + ": " + msg + "\n", obj);
      }
    }
    if (!logConsole && level == LogLevel.ERROR) { // repeat to console
      System.out.printf("JarClassLoader-" + level + ": " + msg + "\n", obj);
    }
  }

  /**
   * Inner class with JAR file information.
   */
  private static class JarFileInfo {
    JarFile jarFile; // this is the essence of JarFileInfo wrapper
    String simpleName; // accumulated for logging like: "topJar!childJar!kidJar"
    File fileDeleteOnExit;
    Manifest mf; // required for package creation
    ProtectionDomain pd;

    /**
     * @param jarFile Never null.
     * @param simpleName Used for logging. Never null.
     * @param jarFileParent Used to make simpleName for logging. Null for top level JAR.
     * @param fileDeleteOnExit Used only to delete temporary file on exit. Could be null if not
     *        required to delete on exit (top level JAR)
     */
    JarFileInfo(JarFile jarFile, String simpleName, JarFileInfo jarFileParent, ProtectionDomain pd,
        File fileDeleteOnExit) {
      this.simpleName = (jarFileParent == null ? "" : jarFileParent.simpleName + "!") + simpleName;
      this.jarFile = jarFile;
      this.pd = pd;
      this.fileDeleteOnExit = fileDeleteOnExit;
      try {
        this.mf = jarFile.getManifest(); // 'null' if META-INF directory is missing
      } catch (IOException ex) {
        // Ignore and create blank manifest
      }
      if (this.mf == null) {
        this.mf = new Manifest();
      }
    }

    String getSpecificationTitle() {
      return mf.getMainAttributes().getValue(Name.SPECIFICATION_TITLE);
    }

    String getSpecificationVersion() {
      return mf.getMainAttributes().getValue(Name.SPECIFICATION_VERSION);
    }

    String getSpecificationVendor() {
      return mf.getMainAttributes().getValue(Name.SPECIFICATION_VENDOR);
    }

    String getImplementationTitle() {
      return mf.getMainAttributes().getValue(Name.IMPLEMENTATION_TITLE);
    }

    String getImplementationVersion() {
      return mf.getMainAttributes().getValue(Name.IMPLEMENTATION_VERSION);
    }

    String getImplementationVendor() {
      return mf.getMainAttributes().getValue(Name.IMPLEMENTATION_VENDOR);
    }

    URL getSealUrl() {
      String seal = mf.getMainAttributes().getValue(Name.SEALED);
      if (seal != null) {
        try {
          return new URL(seal);
        } catch (MalformedURLException ex) {
          // Ignore, will return null
        }
      }
      return null;
    }
  } // inner class JarFileInfo

  /**
   * Inner class with JAR entry information. Keeps JAR file and entry object.
   */
  private static class JarEntryInfo {
    JarFileInfo jarFileInfo;
    JarEntry jarEntry;

    JarEntryInfo(JarFileInfo jarFileInfo, JarEntry jarEntry) {
      this.jarFileInfo = jarFileInfo;
      this.jarEntry = jarEntry;
    }

    URL getUrl() { // used in findResource() and findResources()
      try {
        return new URL("jar:file:" + jarFileInfo.jarFile.getName() + "!/" + jarEntry);
      } catch (MalformedURLException ex) {
        return null;
      }
    }

    String getName() { // used in createTempFile() and loadJar()
      return jarEntry.getName().replace('/', '_');
    }

    @Override
    public String toString() {
      return "JAR: " + jarFileInfo.jarFile.getName() + " ENTRY: " + jarEntry;
    }

    /**
     * Read JAR entry and returns byte array of this JAR entry. This is a helper method to load JAR
     * entry into temporary file.
     *
     * @param inf JAR entry information object
     * @return byte array for the specified JAR entry
     */
    byte[] getJarBytes() throws JarClassLoaderException {
      DataInputStream dis = null;
      byte[] theBy = null;
      try {
        long size = jarEntry.getSize();
        if (size <= 0 || size >= Integer.MAX_VALUE) {
          throw new JarClassLoaderException("Invalid size " + size + " for entry " + jarEntry);
        }
        theBy = new byte[(int) size];
        InputStream is = jarFileInfo.jarFile.getInputStream(jarEntry);
        dis = new DataInputStream(is);
        dis.readFully(theBy);
      } catch (IOException ex) {
        throw new JarClassLoaderException(null, ex);
      } finally {
        if (dis != null) {
          try {
            dis.close();
          } catch (IOException ex) {
            // 忽略之
          }
        }
      }
      return theBy;
    }
  } // inner class JarEntryInfo

  /**
   * Inner class to handle JarClassLoader exceptions.
   */
  @SuppressWarnings("serial")
  private static class JarClassLoaderException extends Exception {
    JarClassLoaderException(String msg) {
      super(msg);
    }

    JarClassLoaderException(String msg, Throwable cause) {
      super(msg, cause);
    }

    String getMessageAll() {
      StringBuilder sb = new StringBuilder();
      for (Throwable e = this; e != null; e = e.getCause()) {
        if (sb.length() > 0) {
          sb.append(" / ");
        }
        String msg = e.getMessage();
        if (msg == null || msg.length() == 0) {
          msg = e.getClass().getSimpleName();
        }
        sb.append(msg);
      }
      return sb.toString();
    }
  } // inner class JarClassLoaderException

} // class JarClassLoader
